Skip to main content

2. Static decorations

One of the feature advantage of Wolfram Mathematica and WLJS Notebook is a multimodal cells with a powerful syntax sugar. A visual representation of an instance of an object makes the programming experience more educative for sure.

Summary Item

The easiest way of providing a bit more information, but still keeping the actual expression intact is to use ArrangeSummaryBox

StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]}
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
None,
summary,
Null
]
]
]

Here we redefined a standard output form to a decorated summary box, providing the visible state field

StateMachine["State" -> 3]

info

Despite the fact of looking different, you can still work with it normally: setting and getting properties, i.e.

is 100% valid

Custom decorations

One can do all decorations from scratch using Graphics for instance

StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
With[{
g = Graphics[{ Opacity[0.5],
Table[
Rotate[{
Hue[i/12.0, 1.0, 0.5],
Rectangle[{-1,-1}, {1,1}]
}, i / (s["State"]+1)]
, {i, 0, 6Pi, Pi}]
}, ImageSize->{100,100}, ImagePadding->None]
},
ViewBox[s, g]
]
]

The result will look like

machine = StateMachine[]

StateMachineChange[machine, 2]

Summary Item and Custom decoration

Why not to merge both leaving the graphics as an icon?

StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]},
icon = Graphics[{ Opacity[0.5],
Table[
Rotate[{
Hue[i/12.0, 1.0, 0.5],
Rectangle[{-1,-1}, {1,1}]
}, i / (s["State"]+1), {0.,0.}]
, {i, 0, 6Pi, Pi}]
}, ImageSize->{50,50}, AspectRatio->1, ImagePadding->None]
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
icon,
summary,
Null
]
]
]
machine = StateMachine["State"->2]

Javascript decoration

There is also an option to use pure Javascript to render an object, let us make our layout much simpler starting with

StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
ViewBox[s, CustomDecorator[s["State"]]]
]

Here we mentioned CustomDecorator which is going to be our WLJS Function

Then, create a new cell

.js

core.CustomDecorator = async (args, env) => {
const state = await interpretate(args[0], env);
const element = env.element;

element.classList.add('flex', 'rounded-md', 'p-2');
element.style.border = "1px solid #999";
element.style.boxShadow = "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)";

element.style.transitionDuration = '0.8s';
element.style.transitionProperty = 'transform';

setTimeout(() => {
element.style.transform = "rotate(360deg)";
}, 100);

element.innerText = state;
}

The result will be following

Animated decoration in Summary Item

Why not also animate it using Wolfram Language?

Let it be many balls bouncing the walls. Firstly let us make proof of concept

test
balls = RandomReal[{-1,1}, {4,2}];
velocities = RandomReal[{-1,1}, {4,2}];

EventHandler["animate", Function[Null,
{balls, velocities} = Map[With[{
v = {If[Abs[#[[1,1]]] >= 1, -1, 1], If[Abs[#[[1, 2]]] >= 1, -1, 1]} #[[2]],
p = #[[1]]
},
{p + 0.2 v, v}
]&, Transpose[{balls, velocities}]] // Transpose;
]]

Graphics[{PointSize[0.03], Point[balls // Offload], AnimationFrameListener[balls // Offload, "Event"->"animate"]}, PlotRange->{{-1,1}, {-1,1}}, TransitionType->None]

Now we need only to scope our variables and embed it to summary item

StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{
balls = RandomReal[{-1,1}, {s["State"],2}],
velocities = RandomReal[{-1,1}, {s["State"],2}],
animateEvent = CreateUUID[]
},

EventHandler[animateEvent, Function[Null,
{balls, velocities} = Map[With[{
v = {If[Abs[#[[1,1]]] >= 1, -1, 1], If[Abs[#[[1, 2]]] >= 1, -1, 1]} #[[2]],
p = #[[1]]
},
{p + 0.2 v, v}
]&, Transpose[{balls, velocities}]] // Transpose;
]];

With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]},
icon = Graphics[{
PointSize[0.03], Point[balls // Offload],
AnimationFrameListener[balls // Offload, "Event"->animateEvent]
},
PlotRange->{{-1,1}, {-1,1}},
TransitionType->None,
ImageSize->{50,50},
AspectRatio->1,
ImagePadding->None
]
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
icon,
summary,
Null
]
]
]

Then let us see the result

machine = StateMachine["State"->2]

If you want to see an optimized version, please, follow below

Optimized version

Since it relies on AnimationFrameListener, it runs as fast as possible, which might be an issue for a lot of those objects on the screen.

Just using SetInterval is not an options, since we need something to remove this timer, when there is no visible widgets.

note

ArrangeSummaryBox is a wrapper over ViewBox, which has an event generator. A user can attach EventHandler to it and check if a widget was destroyed or created.

StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{
balls = RandomReal[{-1,1}, {s["State"],2}],
velocities = RandomReal[{-1,1}, {s["State"],2}],
task, instances = 0,
calculate,
controller = CreateUUID[],
construct,
notebook = EvaluationNotebook[],
destruct
},

(* if someone closed notebook *)
With[{cloned = EventClone[notebook]},
EventHandler[cloned, {"OnClose" -> Function[Null,
destruct;
]}];
];

construct := With[{},
task = SetInterval[calculate[], 100];
];

destruct := With[{},
TaskRemove[task];
];

EventHandler[controller, {
"Mounted" -> Function[Null,

If[instances === 0, construct];
instances = instances + 1;

],

"Destroy" -> Function[Null,
instances = instances - 1;

(* unsubscribe when there is no instances *)
If[instances === 0, destruct];
]
}];

calculate = Function[Null,
{balls, velocities} = Map[With[{
v = {If[Abs[#[[1,1]]] >= 1, -1, 1], If[Abs[#[[1, 2]]] >= 1, -1, 1]} #[[2]],
p = #[[1]]
},
{p + 0.2 v, v}
]&, Transpose[{balls, velocities}]] // Transpose;
];

With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]},
icon = Graphics[{
PointSize[0.03], Point[balls // Offload]
},
PlotRange->{{-1,1}, {-1,1}},
TransitionType->"Linear",
TransitionDuration -> 100,
ImageSize->{50,50},
AspectRatio->1,
ImagePadding->None
]
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
icon,
summary,
Null,

"Event" -> controller
]
]
]

The following changes were made

  • SetInterval drives the calculations with 100 ms interval
  • TransitionDuration interpolates the results with 100 ms window
  • We listen an events of creation and destruction of widgets using "Event" option of ArrangeSummaryBox
  • We remove timers, when there is no visible instances on the screen
  • We remove timers, when the connection to the notebook was lost (a user closed notebook)